<template>
  <div class="wrap">
    <video class="video" muted autoplay controls ref="flvPlayerVideo"></video>
  </div>
</template>

<script setup>
import flvjs from 'flv.js'
import { onMounted, onUnmounted, reactive, ref, watch} from 'vue'

const prop = defineProps({
  url: {
    type: String,
    default: "ws://10.82.14.164:8888/rtsp/1?url=rtsp://admin:Admin123@10.82.14.182/1/h264major",
  },
  option: {
    type: Object,
    default: () => {
      return {
        type: "flv", // 视频类型  flv/mp4
        isLive: true, // 是否实时流
      };
    },
  },
  flvLoading: {
    type: Boolean,
    default: false,
  },
})

const flvPlayerVideo = ref(null)
const state = reactive({
  loading: true,
  flvPlayer: null,
  delayTimer: null,

  maxReconnectCount: Window.reconnectFlvCount, // 重连的最大次数
  errorCount: 0, // 错误次数，连接10次则不再连接
  lastDecodedFrame: 0, // 用于判断卡顿
  endedReloadFlag: true, // 用于判断推流结束，是否要再次构建（情景：后端关闭推流后重新打开，有几秒的断流时间）

  isFlullscreen: false,
});

onMounted(() => {
  // 如果有地址，则直接构建视频
  if (prop.url) {
    flvInitLoad();
  }
});

onUnmounted(() => {
  flvDestory();
});

watch(
  () => [prop.url, state.loading, prop.flvLoading],
  (newValue, oldValue) => {
    const _url = newValue[0],
      _urlOld = oldValue[0],
      _loading = newValue[1],
      _loadingOld = oldValue[1],
      _flvLoading = newValue[2];
    // url为空
    if (!_url) {
      state.loading = false;
      addLog(`address changing, empty address`);
      flvDestory();
    }
    // url改变了
    else if (_url != _urlOld) {
      addLog(`address changing,Old address:${_urlOld},New address:${_url}`);
      flvInitLoad();
    }
    // 如果传入的loading是true，并且url没有变化，则重新构建推流，因为后端是关闭了再打开的
    else if (_flvLoading === true) {
      // addLog(`_flvLoading:${_flvLoading}`);
      ctx.emit("update:flvLoading", false); // 反馈给父组件的loading
    }

    if (_loading != _loadingOld) {
      // addLog(`flv component loading:${_loading}`);
      ctx.emit("update:flvLoading", _loading); // 反馈给父组件的loading
    }
  }
);

/**
 * @description: 报错刷新
 * @return {*}
 * @Author: liuxin
 */
const faultRefresh = () => {
  ctx.emit("faultRefresh"); // 传递给父组件，用于请求后端推流
  flvInitLoad();
}

/**
 * @description: 播放器初始化处理
 * @param {*} handleLoading loading状态
 * @return {*}
 * @Author: liuxin
 */
const flvInitLoad = (handleLoading = true) => {
  state.loading = handleLoading; // loading动画
  state.errorCount = 0; // 错误连接次数置0
  flvDestory();
  addLog(`flv init load,loading:${handleLoading},address:${prop.url}`);
  state.delayTimer = setTimeout(() => {
    flvCreated();
  }, Window.flvTimeOut); // 等待一会儿再打开，防止找不到元素，最好不要超过1000,，否则切换时，容易导致锁死
}


/**
 * @description: 构建播放器
 * @return {*}
 * @Author: liuxin
 */
const flvCreated = () => {
  try {
    const videoElement = flvPlayerVideo.value;
    if (flvjs.isSupported() && videoElement) {
      addLog(`flv created,address:${prop.url}`);
      const flvOption = {
        url: prop.url, // 播放地址
        hasAudio: false, // 是否有音频
        hasVideo: true, //是否有视频
        isLive: true, // 是否是直播流，默认 true
        type: "flv", // 是否是直播流，默认 true
        stashInitialSize: 128, // 减少首帧显示等待时长
        ...prop.option,
      };
      state.flvPlayer = flvjs.createPlayer(flvOption, {
        enableWorker: false, // 不启用分离的线程进行转换，之前为true
        enableStashBuffer: false, // 关闭IO隐藏缓冲区
        stashInitialSize: 128, // 减少首帧显示等待时长
        autoCleanupSourceBuffer: true, // 打开自动清除缓存
        fixAudioTimestampGap: false, //false才会音视频同步,新增
        lazyLoad: false, // 去掉懒加载,新增
      });

      state.flvPlayer.attachMediaElement(videoElement);
      state.flvPlayer.load();
      state.flvPlayer.play();
      state.endedReloadFlag = true; // 重置画面停滞的播放状态，下次停滞了会再次打开
      videoElementEvent(); // 手动跳帧，防止延时
      flvPlayerEvent(); // 断流、卡顿处理
    }
  } catch (error) {
    console.error("构建错误");
  }
}

/**
 * @description: 销毁播放器
 * @return {*}
 * @Author: liuxin
 */
const flvDestory = () => {
  if (state.flvPlayer == null) return;
  addLog(`flv destory,address:${prop.url}`);
  try {
    if (state.flvPlayer._hasPendingLoad) {
      state.flvPlayer.pause();
      state.flvPlayer.unload();
    }
    state.flvPlayer.detachMediaElement();
    state.flvPlayer.destroy();
    state.flvPlayer = null;
    clearTimeout(state.delayTimer); // 清除推迟打开播放器定时器
  } catch (error) {
    console.error("销毁错误");
  }
}

const videoElementEvent = () => {
  const videoElement = flvPlayerVideo.value; // 媒体播放器
  if (!videoElement) {
    return;
  }

  /**
   * @description: 浏览器下载流事件，手动跳帧，防止累计延时
   * @return {*}
   * @Author: liuxin
   */
  videoElement.addEventListener("progress", () => {
    if (state.flvPlayer.buffered.length) {
      let end = state.flvPlayer.buffered.end(0); //获取当前时间值
      let diff = end - state.flvPlayer.currentTime; //获取相差差值
      // 延迟过大或帧率不正常，通过跳帧的方式更新视频
      if (diff > 20 || diff < 0) {
        // addLog(`Manual frame skipping,address:${prop.url}`); // 添加日志
        state.flvPlayer.currentTime = state.flvPlayer.buffered.end(0) - 0.5; // 手动跳帧到最后
        return;
      }

      // 正常帧率，正常播放
      if (diff <= 1) {
        videoElement.playbackRate = 1;
      }
      // 10秒内的延时，1.1倍速播放
      else if (diff <= 10) {
        // addLog(`Chase frames manually 1.1,address:${prop.url}`); // 手动追帧
        videoElement.playbackRate = 1.1;
      }
      // 20秒内的延时，1.2倍速播放
      else if (diff <= 20) {
        // addLog(`Chase frames manually 1.2,address:${prop.url}`); // 手动追帧
        videoElement.playbackRate = 1.2;
      }
    }
  });

  /**
   * @description: 监听停止事件，如果是后端关闭了，再次打开，则需要重新构建，这里从上次打开开始计算，只重连一次
   * @return {*}
   * @Author: liuxin
   */
  videoElement.addEventListener("ended", () => {
    state.loading = false; // 去掉loading动画
    if (state.endedReloadFlag) {
      addLog(`Video ended, reconnecting:${prop.url}`); // 日志
      state.endedReloadFlag = false; // 停滞重载的标识，设置为false，目的是只重载一次，若无法播放，会进入error事件重载
      flvInitLoad(false); // 销毁后重新构建，无loading动画，有延时播放
    }
  });

  /**
   * @description: 监听点击事件，不执行暂停
   * @return {*}
   * @Author: liuxin
   */
  videoElement.addEventListener("click", (e) => {
    e.preventDefault();
  });
}

/**
 * @description: flv对象事件监听，断流、卡顿处理
 * @return {*}
 * @Author: liuxin
 */
const flvPlayerEvent = () => {
  if (!state.flvPlayer._hasPendingLoad) {
    return;
  }

  /**
   * @description: 监听出错消息后，销毁后重连
   * @return {*}
   * @Author: liuxin
   */
  state.flvPlayer.on(
    flvjs.Events.ERROR,
    (errorType, errorDetail, errorInfo) => {
      //视频出错后销毁重新创建 网络错误
      if (state.flvPlayer && state.errorCount <= state.maxReconnectCount) {
        addLog(`Video error ${state.errorCount} reconnection,
            address:${prop.url},
            errorinfo:${JSON.stringify(errorInfo)}`); // 视频报错N重连
        state.errorCount++;
        flvDestory();
        flvCreated();
      }

      if (state.errorCount > state.maxReconnectCount) {
        state.loading = false; // 去掉loading
      }
    }
  );

  /**
   * @description: 视频卡顿，销毁后重建
   * @return {*}
   * @Author: liuxin
   */
  state.flvPlayer.on("statistics_info", (res) => {
    // 初始化播放
    if (state.lastDecodedFrame == 0) {
      state.lastDecodedFrame = res.decodedFrames;
      return;
    }
    // 正常播放
    if (state.lastDecodedFrame != res.decodedFrames) {
      state.lastDecodedFrame = res.decodedFrames;
      if ((state.loading = true)) {
        state.loading = false; // 去掉loading
      }
    }
    // 播放异常
    else {
      if (state.player) {
        addLog(`Reconnect after video freezes, address:${prop.url}`); // 添加日志
        state.errorCount = 0; // 错误连接次数归0
        state.lastDecodedFrame = 0; // 最后播放编码号
        flvDestory(); // 销毁对象
        flvCreated(); // 创建对象
      }
    }
  });
}

/**
 * @description: 添加前后端日志
 * @param {*} log 日志文字
 * @return {*}
 * @Author: liuxin
 */
const addLog = (log) => {
  // console.log(log);
  // 发送消息给后端 存日志
  // scoket.send(
  //   JSON.stringify({
  //     api: "web_log",
  //     data: {
  //       log: log,
  //     },
  //   })
  // );
}

/**
 * @description: 全屏 / 退出全屏
 * @return {*}
 * @Author: liuxin
 */
const fullscreenHandle = () => {
  state.isFlullscreen = !state.isFlullscreen;

  document.addEventListener("keydown", function (e) {
    //此处填写你的业务逻辑即可
    if (e.key == "Escape") {
      e.stopPropagation();
      state.isFlullscreen = false;
    }
  });
}

</script>

<style lang="scss" scoped>
.wrap {
  .video {
    width: 960px;
    height: 540px;
  }
}
</style>
